package plugproxy;

import plugproxy.util.AddonUtil;
import plugproxy.util.HexDump;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Hashtable;

/**
 * Passes TCP/IP data from the local host to remote system:port
 *
 * @author Christopher A. Longo (cal@cloud9.net)
 * @version 2.01 11.22.2002 $Revision: 1.3 $
 */
public class PlugProxyListener implements Runnable {
    /**
     * The parameter structure
     */
    private Parameters params;
    /**
     * Socket for listening
     */
    private ServerSocket listener;
    /**
     * If worker thread is enabled
     */
    private boolean enabled = true;
    /**
     * Used for pushing verbose information
     */
    private ActionListener actionListener;

    /**
     * Information event for verbose mode
     */
    public static int EVENT_INFO = 0x01;
    /**
     * Data event for sniff mode
     */
    public static int EVENT_DATA = 0x02;
    /**
     * Input stream flag
     */
    public static int STREAM_IN = 0x04;
    /**
     * Output stream flag
     */
    public static int STREAM_OUT = 0x08;

    /**
     * Used to manage the child connections
     */
    private ThreadGroup group = new ThreadGroup("PlugProxyGroup");

    /**
     * start a socket listener and create a new thread to listener all request
     *
     * @param params parameters
     * @throws IOException io exception during listener start
     */
    public PlugProxyListener(Parameters params) throws IOException {
        this.params = params;
        listener = new ServerSocket(params.localPort);
        Thread runner = new Thread(this);
        runner.setPriority(Thread.NORM_PRIORITY - 1);
        runner.start();
    }

    /**
     * The main threading routine.
     */
    public void run() {
        while (enabled) {
            try {
                Socket client = listener.accept();
                Socket server = new Socket(params.host, params.remotePort);
                Long timestamp = System.currentTimeMillis();
                //receive data from client and send it to server
                Hashtable<String, Object> context = new Hashtable<String, Object>();
                new PlugProxyThread(context, timestamp, client.toString(), server, client, STREAM_IN, group).start();
                //receive data from server and send it to client
                new PlugProxyThread(context, timestamp, server.toString(), client, server, STREAM_OUT, group).start();
            }
            catch (IOException e) {
                enabled = false;
            }
        }
    }

    /**
     * Stops the listener from forwarding data to the remote host.
     * Shuts down all child threads.
     */
    public void stop() {
        enabled = false;
        try {
            listener.close();
        } catch (IOException ignore) {
        }
        PlugProxyThread[] list = new PlugProxyThread[group.activeCount()];
        group.enumerate(list);
        for (PlugProxyThread thread : list) {
            thread.disable();
        }
    }

    /**
     * Fires off an event to all registered listeners.
     *
     * @param receivedContent A String containing details of the event.
     * @param timestamp       timestamp for request
     * @param modifier        modifier
     */
    protected void fireEvent(long timestamp, String receivedContent, int modifier) {
        if (actionListener != null) {
            if (!receivedContent.endsWith("\n")) receivedContent = receivedContent + "\r\n";
            ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, receivedContent, timestamp, modifier);
            actionListener.actionPerformed(e);
        }
    }

    /**
     * Adds an ActionListener to this object's listener list.
     *
     * @param listener The ActionListener to add.
     */
    public synchronized void addActionListener(ActionListener listener) {
        if (listener != null)
            actionListener = AWTEventMulticaster.add(actionListener, listener);
    }

    /**
     * Removes an ActionListener from this object's listener list.
     *
     * @param l The ActionListener to remove.
     */
    public synchronized void removeActionListener(ActionListener l) {
        if (l != null)
            actionListener = AWTEventMulticaster.remove(actionListener, l);
    }

    /**
     * Returns a String representation of this object.
     *
     * @return A a String representation of this object.
     */
    public String toString() {
        String s = null;
        try {
            s = InetAddress.getLocalHost() + ":" + params.localPort + "->" + params.host + ":" + params.remotePort;
        }
        catch (UnknownHostException ignore) {
        }
        return s;
    }

    /**
     * This inner class is the actual engine for fowarding the data to the remote host.
     */
    class PlugProxyThread extends Thread {
        /**
         * context for running
         */
        private Hashtable<String, Object> context;
        /**
         * timestamp for each request
         */
        private long timestamp;
        /**
         * The size of the buffer to pass data from in->out
         */
        private static final int BUFFER_SIZE = 4096;
        /**
         * The actual data buffer
         */
        private byte[] buffer = new byte[BUFFER_SIZE];
        /**
         * The input stream
         */
        private InputStream in;
        /**
         * The output stream
         */
        private OutputStream out;
        /**
         * Direction of the stream, STREAM_IN or STREAM_OUT
         */
        private int direction;
        /**
         * Source socket
         */
        private Socket source;
        /**
         * Destination socket
         */
        private Socket dest;

        /**
         * Constructor
         *
         * @param context   run context
         * @param timestamp timestamp for request
         * @param name      The name of the thread.
         * @param source    The source stream (in).
         * @param dest      The destination stream (out).
         * @param direction The direction of the stream, STREAM_IN or STREAM_OUT.
         * @param group     thread group
         * @throws IOException Thrown if the an IO exception occurs.
         */
        public PlugProxyThread(Hashtable<String, Object> context, Long timestamp, String name, Socket source, Socket dest, int direction, ThreadGroup group) throws IOException {
            super(group, name);
            this.context = context;
            this.timestamp = timestamp;
            this.source = source;
            this.dest = dest;
            in = source.getInputStream();
            out = dest.getOutputStream();
            this.direction = direction;
            setPriority(NORM_PRIORITY - 1);
        }

        /**
         * Forwards any data from the source to the destination. Simple as that.
         */
        public void run() {
            fireEvent(timestamp, getName() + ": Connection established.", EVENT_INFO | direction);
            try {
                int length;
                while (enabled && (length = in.read(buffer)) != -1) // this is the heart of it all.  Read a chunk of data...
                {
                    out.write(buffer, 0, length); // ... and forward it out.  Everything else is just logging and cleanup
                    if (params.sniff) { //fired the received content to listener if none quiet mode
                        //hexed or plain text
                        String originalContent = new String(buffer, 0, length);
                        String receivedContent = params.hex ? HexDump.toHex(buffer, length) + '\n' : originalContent;
                        boolean ignoredRequest = context.get("ignoredRequest") != null;
                        if (params.http && params.httpFilter != null && this.direction == STREAM_OUT && originalContent.contains(" HTTP/")) {
                            String requestUri = originalContent.substring(0, originalContent.indexOf("HTTP/"));
                            requestUri = requestUri.substring(requestUri.indexOf(" ")).trim();
                            if (!AddonUtil.isWildMatched(requestUri, params.httpFilter)) {
                                ignoredRequest = true;
                                context.put("ignoredRequest", "true");
                            } else {
                                ignoredRequest = false;
                                context.remove("ignoredRequest");
                            }
                        }
                        if (!ignoredRequest)
                            fireEvent(timestamp, receivedContent, EVENT_DATA | direction);  // text mode
                    }
                }
            } catch (IOException ignore) {
                ignore.printStackTrace();
            }
            finally {
                cleanUp();
                fireEvent(timestamp, getName() + ": Connection terminated.", EVENT_INFO | direction);
            }
        }

        /**
         * Stops this thread from further prosessing data.
         * Closes the blocking input stream.
         */
        public void disable() {
            cleanUp();
        }

        /**
         * Cleans up and closes all connections.
         */
        private void cleanUp() {
            try {
                out.flush();
            } catch (Exception ignore) {
            }
            try {
                out.close();
            } catch (Exception ignore) {
            }
            try {
                in.close();
            } catch (Exception ignore) {
            }
            try {
                dest.close();
            } catch (Exception ignore) {
            }
            try {
                source.close();
            } catch (Exception ignore) {
            }
            in = null;
            out = null;
            source = null;
            dest = null;
        }
    }
}
